<!--
@license
Copyright 2016 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../tf-imports/plottable.html">
<link rel="import" href="../tf-imports/lodash.html">

<!--
vz-line-chart creates an element that draws a line chart for
displaying event values.

This line chart supports drawing multiple lines at the same time, with features
such as different X scales (linear and temporal), tooltips and smoothing.

@element vz-line-chart
@demo demo/index.html
-->
<dom-module id="vz-line-chart">
  <template>
    <div id="tooltip">
      <table>
        <thead>
          <tr>
            <th></th>
            <th>Name</th>
            <template is="dom-if" if="{{smoothingEnabled}}">
              <th>Smoothed</th>
            </template>
            <th>Value</th>
            <th>Step</th>
            <th>Time</th>
            <th>Relative</th>
          </tr>
        </thead>
        <tbody>
        </tbody>
      </table>
    </div>
    <svg id="chartsvg"></svg>
    <style>
      :host {
        -webkit-user-select: none;
        -moz-user-select: none;
        display: flex;
        flex-direction: column;
        flex-grow: 1;
        flex-shrink: 1;
        position: relative;
        outline: none;
      }
      svg {
        -webkit-user-select: none;
        -moz-user-select: none;
        flex-grow: 1;
        flex-shrink: 1;
      }
      td {
        padding-left: 5px;
        padding-right: 5px;
        font-size: 13px;
        opacity: 1;
      }
      #tooltip {
        pointer-events: none;
        position: absolute;
        opacity: 0;
        box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
        font-size: 14px;
        background: rgba(0, 0, 0, 0.8);
        color: white;
        border-radius: 4px;
        line-height: 1.4em;
        padding: 8px;
        z-index: 5;
        cursor: none;
        margin-top: 10px;
      }
      .swatch {
        border-radius: 50%;
        width: 14px;
        height: 14px;
        display: block;
        border: 2px solid rgba(0,0,0,0);
      }
      .closest .swatch {
        border: 2px solid white;
      }
      th {
        padding-left: 5px;
        padding-right: 5px;
        text-align: left;
      }
      .distant td {
        opacity: 0.8;
      }

      .distant td.swatch {
        opacity: 1;
      }

      .ghost {
        opacity: 0.2;
        stroke-width: 1px;
      }

    </style>
  </template>
  <script src="dragZoomInteraction.js"></script>
  <script src="vz-line-chart.js"></script>
  <script src="vz-chart-helpers.js"></script>
  <script>
    Polymer({
      is: "vz-line-chart",
      properties: {
        /**
         * Scale that maps series names to colors. The default colors are from
         * d3.scale.category10() scale. Use this property to replace the default
         * line colors with colors of your own choice.
         * @type {Plottable.Scales.Color}
         * @required
         */
        colorScale: {
          type: Object,
          value: function() {
            return new Plottable.Scales.Color()
                .range(d3.scale.category10().range());
          }
        },

        /**
         * Whether smoothing is enabled or not. If true, smoothed lines will be
         * plotted in the chart while the unsmoothed lines will be ghosted in
         * the background.
         *
         * The smoothing algorithm is a simple moving average, which, given a
         * point p and a window w, replaces p with a simple average of the
         * points in the [p - floor(w/2), p + floor(w/2)] range.  If there
         * aren't enough points to cover the entire window to the left, the
         * window is reduced to fit exactly the amount of elements available.
         * This means that the smoothed line will be less in and gradually
         * become more smooth until the desired window is reached. However when
         * there aren't enough points on the right, the line stops being
         * rendered at all.
         */
        smoothingEnabled: {
          type: Boolean,
          value: false
        },

        /**
         * Weight (between 0.0 and 1.0) of the smoothing. This weight controls
         * the window size, and a weight of 1.0 means using 50% of the entire
         * dataset as the window, while a weight of 0.0 means using a window of
         * 0 (and thus replacing each point with themselves).
         *
         * The growth between 0.0 and 1.0 is not linear though. Because
         * changing the window from 0% to 30% of the dataset smooths the line a
         * lot more than changing the window from 70% to 100%, an exponential
         * function is used instead: http://i.imgur.com/bDrhEZU.png. This
         * function increases the size of the window slowly at the beginning
         * and gradually speeds up the growth, but 0.0 still means a window of
         * 0 and 1.0 still means a window of the dataset's length.
         */
        smoothingWeight: {
          type: Number,
          value: 0.6
        },

        /**
         * The way to display the X values. Allows:
         * - "step" - Linear scale using the  "step" property of the datum.
         * - "wall_time" - Temporal scale using the "wall_time" property of the
         * datum.
         * - "relative" - Temporal scale using the "relative" property of the
         * datum if it is present or calculating from "wall_time" if it isn't.
         */
        xType: {
          type: String,
          value: 'step'
        },

        /**
         * The scale for the y-axis. Allows:
         * - "linear" - linear scale (Plottable.Scales.Linear)
         * - "log" - modified-log scale (Plottable.Scales.ModifiedLog)
         */
        yScaleType: {
          type: String,
          value: 'linear'
        },

        /**
         * Change how the tooltip is sorted. Allows:
         * - "default" - Sort the tooltip by input order.
         * - "ascending" - Sort the tooltip by ascending value.
         * - "descending" - Sort the tooltip by descending value.
         * - "nearest" - Sort the tooltip by closest to cursor.
         */
        tooltipSortingMethod: {
          type: String,
          value: 'default'
        },

        /**
         * Change how the tooltip is positioned. Allows:
         * - "bottom" - Position the tooltip on the bottom of the chart.
         * - "right" - Position the tooltip to the right of the chart.
         */
        tooltipPosition: {
          type: String,
          value: 'bottom'
        },

        _attached: Boolean,
        _chart: Object,
        _visibleSeriesCache: {
          type: Array,
          value: function() { return [] }
        },
        _seriesDataCache: {
          type: Object,
          value: function() { return {} }
        },
        _makeChartAsyncCallbackId: {
          type: Number,
          value: null
        }
      },
      observers: [
        "_makeChart(xType, yScaleType, colorScale, _attached)",
        "_reloadFromCache(_chart)",
        "_smoothingChanged(smoothingEnabled, smoothingWeight, _chart)",
        "_tooltipSortingMethodChanged(tooltipSortingMethod, _chart)",
        "_tooltipPositionChanged(tooltipPosition, _chart)"
      ],

      /**
       * Sets the series that the chart displays. Series with other names will
       * not be displayed.
       *
       * @param {String[]} names Array with the names of the series to
       * display.
       */
      setVisibleSeries: function(names) {
        this._visibleSeriesCache = names;
        if (this._chart) {
          this._chart.setVisibleSeries(names);
          this.redraw();
        }
      },

      /**
       * Sets the data of one of the series. Note that to display this series
       * its name must be in the setVisibleSeries() array.
       *
       * @param {String} name Name of the series.
       * @param {VZ.ChartHelpers.ScalarDatum[]} data Data of the series. This is
       * an array of objects with at least the following properties:
       * - step: (Number) - index of the datum.
       * - wall_time: (Date) - Date object with the datum's time.
       * - scalar: (Number) - Value of the datum.
       */
      setSeriesData: function(name, data) {
        this._seriesDataCache[name] = data;
        if (this._chart) {
          this._chart.setSeriesData(name, data);
        }
      },

      /**
       * Re-renders the chart. Useful if e.g. the container size changed.
       */
      redraw: function() {
        this._chart.redraw();
      },
      attached: function() {
        this._attached = true;
      },
      detached: function() {
        this._attached = false;
      },
      ready: function() {
        this.scopeSubtree(this.$.tooltip, true);
        this.scopeSubtree(this.$.chartsvg, true);
      },
      _makeChart: function(xType, yScaleType, colorScale, _attached) {
        if (this._makeChartAsyncCallbackId !== null) {
          this.cancelAsync(this._makeChartAsyncCallbackId);
          this._makeChartAsyncCallbackId = null;
        }

        this._makeChartAsyncCallbackId = this.async(function() {
          this._makeChartAsyncCallbackId = null;
          if (!this._attached) return;
          if (this._chart) this._chart.destroy();
          var tooltip = d3.select(this.$.tooltip);
          var chart = new VZ.LineChart(xType, yScaleType, colorScale, tooltip);
          var svg = d3.select(this.$.chartsvg);
          chart.renderTo(svg);
          this._chart = chart;
        }, 350);
      },
      _reloadFromCache: function() {
        if(this._chart) {
          this._chart.setVisibleSeries(this._visibleSeriesCache);
          this._visibleSeriesCache.forEach(function(name) {
            this._chart.setSeriesData(name, this._seriesDataCache[name] || []);
          }.bind(this));
        }
      },
      _smoothingChanged: function() {
        if(!this._chart) {
          return;
        }
        if(this.smoothingEnabled) {
          this._chart.smoothingUpdate(this.smoothingWeight);
        }
        else {
          this._chart.smoothingDisable();
        }
      },
      _tooltipSortingMethodChanged: function() {
        if(this._chart) {
          this._chart.setTooltipSortingMethod(this.tooltipSortingMethod);
        }
      },
      _tooltipPositionChanged: function() {
        if (this._chart) {
          this._chart.setTooltipPosition(this.tooltipPosition);
        }
      }
    });
  </script>
</dom-module>
